package org.matchit.selection;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.List;
import java.util.Map;

import org.hamcrest.Matcher;
import org.matchit.converter.SameInstanceConverter;
import org.matchit.converter.bean.BeanToPropertyConverter;
import org.springframework.core.convert.converter.Converter;

/**
 * Allows elements to be selected from a collection, using {@link Matcher} logic.
 * Matches can also be converted, resulting in any desired format.
 * Usage of this selector goes as follows:
 * 
 * <pre>
 *  longNames = from(persons).select("name", String.class).where(hasLongName);
 *  keys = fromMap(map).select(key()).where(value(matches("[a-g]{8}")));
 * </pre>
 * 
 * @author Jeroen van Schagen
 * @since 20-03-2011
 *
 * @param <I> type of input elements
 * @param <O> type of output elements
 */
public class ConvertingSelector<I, O> {
    private final Selector<I> selector;
    private final Converter<? super I, O> converter;

    /**
     * Construct a new {@link ConvertingSelector}.
     * @param selector wrapped to find matching candidates
     * @param converter used to convert matches into the desired output
     */
    private ConvertingSelector(Selector<I> selector, Converter<? super I, O> converter) {
        this.selector = selector;
        this.converter = converter;
    }

    // Construction

    /**
     * Construct a new {@link ConvertingSelector}, based on a collection of content.
     * @param <T> type of content being filtered on
     * @param content collection of the content we can filter on
     * @return a new selector, used to filter content
     */
    public static <T> ConvertingSelector<T, T> from(Collection<T> content) {
        return new ConvertingSelector<T, T>(Selector.from(content), new SameInstanceConverter<T>());
    }

    /**
     * Construct a new {@link ConvertingSelector}, based on an array of content.
     * @param <T> type of content being filtered on
     * @param elements array of the content we can filter on
     * @return a new selector, used to filter elements
     */
    public static <T> ConvertingSelector<T, T> fromElements(T... elements) {
        return from(Arrays.asList(elements));
    }

    /**
     * Construct a new {@link ConvertingSelector}, based on a map.
     * @param <K> type of keys used in map
     * @param <V> type of values used in map
     * @param map contains the entries we can filter on
     * @return a new selector, used to filter entries
     */
    public static <K, V> ConvertingSelector<Map.Entry<K, V>, Map.Entry<K, V>> fromMap(Map<K, V> map) {
        return from(map.entrySet());
    }

    // Restriction

    /**
     * Construct a new {@link ConvertingSelector}, which contains only matching elements.
     * @param matcher provides the matching criteria
     * @return new selector which contains only matching elements
     */
    public ConvertingSelector<I, O> only(Matcher<? super I> matcher) {
        final List<I> rawMatches = selector.where(matcher);
        return new ConvertingSelector<I, O>(Selector.from(rawMatches), converter);
    }

    // Selection

    /**
     * Describe how the matching elements should be returned.
     * @param <X> type of output format
     * @param converter describes how matching elements should be converted
     * @return new selector, which converts matches into the desired format
     */
    public <X> ConvertingSelector<I, X> select(Converter<? super I, X> converter) {
        return new ConvertingSelector<I, X>(selector, converter);
    }

    /**
     * Describe which property, of the matching elements, should be returned.
     * @param <X> type of output format
     * @param propertyName name of the property that should be returned
     * @param propertyClass type of the property that should be returned
     * @return new selector, which converts matches into the desired format
     */
    public <X> ConvertingSelector<I, X> select(String propertyName, Class<X> propertyClass) {
        return this.select(new BeanToPropertyConverter<I, X>(propertyName));
    }

    // Retrieval

    /**
     * Retrieve all content, performing no restrictions.
     * @return each content element, in list format
     */
    public List<O> list() {
        return convertToOutputFormat(selector.list());
    }

    /**
     * Retrieve all content, that satisfies our matcher criteria.
     * @param matcher provides the matching criteria
     * @return each satisfying content element, in list format
     */
    public List<O> where(Matcher<? super I> matcher) {
        return convertToOutputFormat(selector.where(matcher));
    }

    /**
     * Convert a collection elements into the desired output format.
     * @param input elements that should be converted
     * @return the converted elements
     */
    private List<O> convertToOutputFormat(Collection<I> input) {
        List<O> output = new ArrayList<O>();
        for (I element : input) {
            output.add(converter.convert(element));
        }
        return output;
    }

}
